/*
 * App.java
 *
 * Created on June 8, 2007, 2:52 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package org.atomojo.app;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.sql.SQLException;
import java.util.Date;
import java.util.Iterator;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.atomojo.app.auth.User;
import org.atomojo.app.db.DB;
import org.atomojo.app.db.DB.MediaEntryListener;
import org.atomojo.app.db.Entry;
import org.atomojo.app.db.EntryMedia;
import org.atomojo.app.db.Feed;
import org.atomojo.app.db.Term;
import org.infoset.xml.Document;
import org.infoset.xml.Element;
import org.infoset.xml.XMLException;
import org.infoset.xml.util.XMLWriter;
import org.restlet.data.MediaType;
import org.restlet.data.Status;
import org.restlet.representation.Representation;
import org.restlet.service.MetadataService;

/**
 *
 * @author alex
 */
public class App
{
   
   public final static String DB_ATTR = "org.atomojo.app.db";
   public final static String STORAGE_ATTR = "org.atomojo.app.storage";
   public final static String RESOURCE_BASE_ATTR = "org.atomojo.app.resource-base";
   public final static String AUTH_SERVICE_ATTR = "org.atomojo.app.auth-service";
   public final static String USER_ATTR = "org.atomojo.app.user";

   public static String join(String [] values, int start, int length,char delimiter) {
      StringBuilder buffer = new StringBuilder();
      int end = start+length;
      for (int i=start; i<end; i++) {
         if (i!=start) {
            buffer.append(delimiter);
         }
         buffer.append(values[i]);
      }
      return buffer.toString();
   }
   
   Logger log;
   DB db;
   Storage storage;
   MetadataService metaService;
   
   /** Creates a new instance of App */
   public App(Logger log,DB db,Storage storage,MetadataService metaService)
   {
      this.log = log;
      this.db = db;
      this.storage = storage;
      this.metaService = metaService;
   }
   
   public Storage getStorage() {
      return storage;
   }
   
   public DB getDB() {
      return db;
   }
   
   public Feed createFeed(String feedPath,Document feedDoc)
      throws AppException
   {
      String [] segments = feedPath.split("/");

      FeedIndex index = AtomResource.indexFeed(log,feedDoc.getDocumentElement());

      // Find the existing paths
      Feed parent = null;
      try {
         parent = db.getRoot();
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to get root feed  due to exception.",ex);
      }
      
      if (parent==null && feedPath.length()==0) {
         try {
            parent = db.createRoot();
            
            // This is the actual feed we're creating, so massage the
            // document
            AtomResource.mergeFeedDocument(feedDoc,parent.getUUID(),parent.getCreated(),parent.getEdited());
            log.fine("Creating root...");
            
            Status status = storage.storeFeed(parent.getPath(),parent.getUUID(),feedDoc);
            if (!status.isSuccess()) {
               throw new AppException(status,"Failed to create root feed.");
            }
         } catch (IOException ex) {
            throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to create root feed  due to exception.",ex);
         } catch (SQLException ex) {
            throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to create root feed  due to exception.",ex);
         }
      } else {
      
         if (parent==null) {
            
            try {
               parent = db.createRoot();
            } catch (SQLException ex) {
               throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to get root feed  due to exception.",ex);
            }
            
            log.fine("Creating root feed.");
            try {
               Status status = storage.storeFeed(parent.getPath(),parent.getUUID(),AtomResource.createFeedDocument("",parent.getUUID(),parent.getCreated()));
               if (!status.isSuccess()) {
                  throw new AppException(status,"Failed to create root feed");
               }
            } catch (XMLException ex) {
               throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to create root feed due to exception.",ex);
            } catch (IOException ex) {
               throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to create root feed due to exception.",ex);
            } catch (SQLException ex) {
               throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to create root feed due to exception.",ex);
            }
         }
         
         if (feedPath.length()==0) {
            throw new AppException(Status.CLIENT_ERROR_CONFLICT,"The root feed already exists.");
         }
         
         int current = 0;
         for (; current<segments.length; current++) {
            try {
               Feed next = parent.getChild(segments[current]);
               if (next==null) {
                  break;
               } else {
                  parent = next;
               }
            } catch (SQLException ex) {
               throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot get segment "+segments[current]+" from database due to exception.",ex);
            }
         }

         // If we found the whole path, the feed alreayd exists
         if (current==segments.length) {
            throw new AppException(Status.CLIENT_ERROR_CONFLICT,"Feed already exists at path: "+feedPath);
         }

         if (log.isLoggable(Level.FINE)) {
            log.fine("Parentage '"+join(segments,0,current,'/')+"' already exists...");
         }

         // Create all the path segments
         for (int i=current; i<segments.length; i++) {
            String path = join(segments,0,i+1,'/');
            if (log.isLoggable(Level.FINE)) {
               log.fine("Creating path: "+path);
            }
            boolean last = i==(segments.length-1);
            boolean created = false;
            try {
               Feed next = parent.getChild(segments[i]);
               if (next!=null && last) {
                  // This shouldn't happen
                  throw new AppException(Status.CLIENT_ERROR_CONFLICT,"Collection already exists at "+path);
               }
               if (next==null) {
                  parent = parent.createChild(segments[i],last ? index.getId() : null);
                  created = true;
               } else {
                  parent = next;
               }
            } catch (SQLException ex) {
               throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot create segment "+segments[i]+" due to SQL exception.",ex);
            }
            // Create the feeds along the way
            if (last) {
               // This is the actual feed we're creating, so massage the
               // document
               AtomResource.mergeFeedDocument(feedDoc,parent.getUUID(),parent.getCreated(),parent.getEdited());
            }

            if (created) {
               if (log.isLoggable(Level.FINE)) {
                  log.fine("Creating feed for: "+path);
               }
               try {
                  Status status = storage.storeFeed(parent.getPath(),parent.getUUID(),last ? feedDoc : AtomResource.createFeedDocument(segments[i],parent.getUUID(),parent.getCreated()));
                  if (!status.isSuccess()) {
                     throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to create feed at path "+path);
                  }
               } catch (XMLException ex) {
                  throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to create feed at path "+path+" due to exception.",ex);
               } catch (IOException ex) {
                  throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to create feed at path "+path+" due to exception.",ex);
               } catch (SQLException ex) {
                  throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to create feed at path "+path+" due to exception.",ex);
               }
            }
         }
      }

      // Index the feed categories
      try {
         for (URI term : index.keySet()) {
            Object value = index.get(term);
            Term t = parent.getDB().createTerm(term);
            parent.categorize(t,value);
         }
      } catch (SQLException ex) {
         log.log(Level.SEVERE,"Cannot categorize entry due to SQL Exception.  Continuing...",ex);
      }
      
      return parent;
      
   }
   
   public Feed getFeed(String path)
      throws AppException
   {
      String [] segments = path.split("/");
      try {
         Feed f = db.findFeedByPath(segments);
         if (f==null) {
            throw new AppException(Status.CLIENT_ERROR_NOT_FOUND,"Feed at path '"+path+"' cannot be found.");
         }
         return f;
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot feed segments from database due to exception.",ex);
      }

   }
   
   public void updateFeed(Feed feed,Document doc)
      throws AppException
   {
      // Check to make sure the document is an entry
      Element top = doc.getDocumentElement();
      if (!top.getName().equals(AtomResource.FEED_NAME)) {
         throw new AppException(Status.CLIENT_ERROR_BAD_REQUEST,"Document element is not a feed: "+top.getName());
      }

      try {
         feed.edited();
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot mark feed as edited.",ex);
      }

      // Index the feed categories
      FeedIndex index = AtomResource.indexFeed(log,top);
      try {
         feed.uncategorize();
         for (URI term : index.keySet()) {
            Object value = index.get(term);
            Term t = feed.getDB().createTerm(term);
            feed.categorize(t,value);
         }
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot categorize entry due to SQL Exception.",ex);
      }

      AtomResource.mergeFeedDocument(doc,feed.getUUID(),feed.getCreated(),feed.getEdited());

      try {
         Status status = storage.storeFeed(feed.getPath(),feed.getUUID(),doc);
         if (!status.isSuccess()) {
            throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot store udpated XML");
         }
      } catch (IOException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot store updated XML.",ex);
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Database error while updating feed.",ex);
      }
   }
   
   public void delete(Feed feed)
      throws AppException
   {
      try {
         String path = feed.getPath();
         feed.delete();
         if (!storage.deleteFeed(path,feed.getUUID())) {
            throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot delete feed collection.");
         }
      } catch (IOException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot delete feed from storage.",ex);
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Database error while deleting feed.",ex);
      }
   }
   
   public Entry createEntry(User user,Feed feed,Document entryDoc) 
      throws AppException
   {
      
      // Check for non-entry document elements
      Element top = entryDoc.getDocumentElement();
      if (!top.getName().equals(AtomResource.ENTRY_NAME)) {
         throw new AppException(Status.CLIENT_ERROR_BAD_REQUEST,"Only entries can be posted to feeds with the Atom mime type.  Found: "+top.getName());
      }

      EntryIndex index = AtomResource.indexEntry(log,top);

      if (index.getId()!=null) {
         try {
            if (feed.findEntry(index.getId())!=null) {
               index.setId(UUID.randomUUID());
            }
         } catch (SQLException ex) {
            throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot find entry in database.",ex);
         }
      } else {
         index.setId(UUID.randomUUID());
      }

      // Create entry index
      Entry entry = null;
      try {
         entry = feed.createEntry(index.getId());
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot create entry in database.",ex);
      }

      try {
         for (URI term : index.keySet()) {
            Object value = index.get(term);
            Term t = feed.getDB().createTerm(term);
            entry.categorize(t,value);
         }
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot categorize entry due to SQL Exception.",ex);
      }

      // Get the identity's author
      String authorName = user.getName();

      // Store in XML DB
      AtomResource.mergeEntry(top,entry.getUUID(),entry.getCreated(),entry.getEdited(),authorName,null);

      try {
         Status status = storage.storeEntry(feed.getPath(),feed.getUUID(),entry.getUUID(),entryDoc);
         if (!status.isSuccess()) {
            String xml = null;
            try {
               StringWriter w = new StringWriter();
               XMLWriter.writeDocument(entryDoc,w);
               xml = w.toString();
               //log.info(xml);
            } catch (Exception ex) {
               // TODO: need to delete
               log.log(Level.SEVERE,"Cannot serialize entry for error message.",ex);
            }
            throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot store entry, status="+status.getCode()+"\n"+xml);
         }
         
         storage.feedUpdated(feed.getPath(),feed.getUUID(),feed.getEdited());
         
      } catch (SQLException ex) {
         try {
            entry.delete(null);
         } catch (SQLException ox) {
            log.log(Level.SEVERE,"Cannot delete entry for exception cleanup.",ox);
         }
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot store entry.",ex);
      } catch (IOException ex) {
         try {
            entry.delete(null);
         } catch (SQLException ox) {
            log.log(Level.SEVERE,"Cannot delete entry for exception cleanup.",ox);
         }
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Database error while storing entry.",ex);
      }
      
      return entry;
   }
   
   public Entry createMediaEntry(User user,Feed feed, Representation entity, String slug, UUID id)
      throws AppException
   {
      // Create entry index
      Entry entry = null;
      try {
         entry = feed.createEntry(id);
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot create entry in database.",ex);
      }

      String file = slug;
      
      try {
         InputStream is = entity.getStream();
         MediaType mediaType = entity.getMediaType();
         if (mediaType==null) {
            throw new AppException(Status.CLIENT_ERROR_BAD_REQUEST,"The media type is missing.");
         }
         MediaType baseMediaType = mediaType.valueOf(mediaType.getName());
         
         // Make sure we have a filename for the media resource
         if (file==null) {
            String ext = metaService.getExtension(baseMediaType);
            if (ext!=null) {
               file = entry.getUUID()+"."+ext;
            } else {
               file = entry.getUUID().toString();
            }
            slug = entry.getUUID().toString();
         } else if (file.indexOf('.')<0) {
            String ext = metaService.getExtension(baseMediaType);
            if (ext!=null) {
               file += "."+ext;
            }
         }
         EntryMedia media;
         try {
            media = entry.createResource(file,mediaType);
         } catch (SQLException ex) {
            try {
               entry.delete(null);
            } catch (SQLException ox) {
               log.log(Level.SEVERE,"Cannot delete entry for exception cleanup.",ox);
            }
            throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot create entry media resource in database.",ex);
         }
         if (media==null) {
            throw new AppException(Status.CLIENT_ERROR_BAD_REQUEST,"Media entry name "+file+" refused.");
         }

         // Get author name for identity
         String authorName = user.getName();

         // Create entry document
         Document doc = null;
         try {
            String title = URLDecoder.decode(slug,"UTF-8");
            doc = AtomResource.createMediaEntryDocument(title,entry.getUUID(),entry.getCreated(),authorName,file,mediaType);
         } catch (XMLException ex) {
            try {
               entry.delete(null);
            } catch (SQLException ox) {
               log.log(Level.SEVERE,"Cannot delete entry for exception cleanup.",ox);
            }
            throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot create media entry document.",ex);
         } catch (UnsupportedEncodingException ex) {
            try {
               entry.delete(null);
            } catch (SQLException ox) {
               log.log(Level.SEVERE,"Cannot delete entry for exception cleanup.",ox);
            }
            throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot decode slug for media entry title.",ex);
         }

         try {
            String path = feed.getPath();
            Status entryStatus = storage.storeEntry(path,feed.getUUID(),entry.getUUID(),doc);
            if (entryStatus.isSuccess()) {
               Status mediaStatus = storage.storeMedia(path,feed.getUUID(),media.getName(),mediaType,is);
               // TODO: storage may change media type parameters and the entry needs to be updated
               if (!mediaStatus.isSuccess()) {
                  try {
                     entry.delete(null);
                  } catch (SQLException ox) {
                     log.log(Level.SEVERE,"Cannot delete entry for exception cleanup.",ox);
                  }
                  try {
                     storage.deleteEntry(path,feed.getUUID(),entry.getUUID());
                  } catch (IOException ex) {
                     log.log(Level.SEVERE,"Cannot delete entry storage for exception cleanup.",ex);
                  }
                  throw new AppException(mediaStatus,"Cannot store entry's media (refused)");
               } else {
                  storage.feedUpdated(feed.getPath(),feed.getUUID(),feed.getEdited());
               }
            } else {
               try {
                  entry.delete(null);
               } catch (SQLException ox) {
                  log.log(Level.SEVERE,"Cannot delete entry for exception cleanup.",ox);
               }
               throw new AppException(entryStatus,"Cannot store entry (refused)");
            }
         } catch (SQLException ex) {
            try {
               entry.delete(null);
            } catch (SQLException ox) {
               log.log(Level.SEVERE,"Cannot delete entry for exception cleanup.",ox);
            }
            throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot store entry.",ex);
         }
      } catch (IOException ex) {
         try {
            entry.delete(null);
         } catch (SQLException ox) {
            log.log(Level.SEVERE,"Cannot delete entry for exception cleanup.",ox);
         }
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot store entry due to request I/O exception.",ex);
      }
      return entry;
   }
   
   public Representation getEntryRepresentation(String feedBaseURI,Feed feed, UUID id)
      throws AppException
   {
      try {
         Entry entry = feed.findEntry(id);
         if (entry==null) {
            throw new AppException(Status.CLIENT_ERROR_NOT_FOUND,"Entry "+id+" not found.");
         }
         return getEntryRepresentation(feedBaseURI,entry);
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot get entry XML.",ex);
      }
   }
   
   public Representation getEntryRepresentation(String feedBaseURI,Entry entry)
      throws AppException
   {
      try {
         Feed feed = entry.getFeed();
         return storage.getEntry(feedBaseURI,feed.getPath(),feed.getUUID(),entry.getUUID());
      } catch (IOException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot get entry XML.",ex);
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot get entry XML.",ex);
      }
   }
   
   public Entry updateEntry(User user,Feed feed, UUID entryId,Document doc)
      throws AppException
   {
      try {
         Entry entry = feed.findEntry(entryId);
         return updateEntry(user,feed,entry,doc);
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot get entry "+entryId+" from feed "+feed.getUUID(),ex);
      }
      
   }
   public Entry updateEntry(User user,Feed feed, Entry entry,Document doc)
      throws AppException
   {
      
      // Check to make sure the document is an entry
      Element top = doc.getDocumentElement();
      if (!top.getName().equals(AtomResource.ENTRY_NAME)) {
         throw new AppException(Status.CLIENT_ERROR_BAD_REQUEST,"Document element is not an entry: "+top.getName());
      }

      // Set the modification time
      Date modified = null;
      
      try {
         modified = entry.edited();
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot mark entry as edited.",ex);
      }

      try {
         EntryIndex index = AtomResource.indexEntry(log,top);
         entry.uncategorize();
         for (URI term : index.keySet()) {
            Object value = index.get(term);
            Term t = entry.getDB().createTerm(term);
            entry.categorize(t,value);
         }
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot categorize entry due to SQL Exception.",ex);
      }

      EntryMedia media = null;
      try {
         Iterator<EntryMedia> resources = entry.getResources();
         if (resources.hasNext()) {
            media = resources.next();
            while (resources.hasNext()) {
               resources.next();
            }
         }
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot check for media resources for entry.",ex);
      }

      // Get author name for identity
      String authorName = user.getName();


      // merge the data with the new entry
      AtomResource.mergeEntry(top,entry.getUUID(),entry.getCreated(),modified,authorName,media);

      try {
         Status status = storage.storeEntry(feed.getPath(),feed.getUUID(),entry.getUUID(),doc);
         if (!status.isSuccess()) {
            throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot store entry document.");
         }
         storage.feedUpdated(feed.getPath(),feed.getUUID(),feed.getEdited());
      } catch (IOException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot store entry document.",ex);
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Database error while storing entry document.",ex);
      }
      return entry;
   }
   
   public void deleteEntry(Feed feed,UUID id)
      throws AppException
   {
      try {
         Entry entry = feed.findEntry(id);
         deleteEntry(feed,entry);
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot get entry "+id,ex);
      }
   }
   
   public void deleteEntry(final Feed feed,Entry entry)
      throws AppException
   {
      String path;
      try {
         path = feed.getPath();
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot get feed path.",ex);
      }
      final String fpath = path;
      try {
         entry.delete(new MediaEntryListener() {
            public void onDelete(EntryMedia resource) {
               try {
                  storage.deleteMedia(fpath,feed.getUUID(),resource.getName());
               } catch (IOException ex) {
                  log.log(Level.SEVERE,"Cannot delete media: "+resource.getName(),ex);
               }
            }
         });
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot enumerate entry media.",ex);
      }
      try {
         if (!storage.deleteEntry(fpath,feed.getUUID(),entry.getUUID())) {
            throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot delete entry document.");
         }
         Date date = feed.touch();
         storage.feedUpdated(feed.getPath(),feed.getUUID(),date);
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot update feed updated element",ex);
      } catch (IOException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot delete entry document",ex);
      }
   }
   
   public void updateMedia(Feed feed, String file, Representation entity)
      throws AppException
   {
      try {
         EntryMedia media = feed.findEntryResource(file);

         MediaType mediaType = entity.getMediaType();
         mediaType = MediaType.valueOf(mediaType.getName());
         media.setMediaType(mediaType);
         storage.storeMedia(feed.getPath(),feed.getUUID(),media.getName(),mediaType,entity.getStream());
         media.edited();
      } catch (IOException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot update entry media "+file,ex);
      } catch (SQLException ex) {
         throw new AppException(Status.SERVER_ERROR_INTERNAL,"Database error while updating entry media "+file,ex);
      }
      
   }
   
   
}
